#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmdata/dcsequen.h"

#include "nlohmann/json.hpp"

#include "util.h"
#include "dataset.h"

#include "network.h"


using json = nlohmann::ordered_json;

#define MY_AETITLE                                 "MARVELSTONE"
static const int timeOut                           = 30;

static const char* transferSyntax[] = {
    UID_LittleEndianImplicitTransferSyntax,
    UID_LittleEndianExplicitTransferSyntax
};
static int transferSyntaxCount = 2;

PWorklistCallback Network::myWorklistCallback = nullptr;
PStoreCallback Network::myStoreCallback = nullptr;

Network::Network()
{
    myAssoc = nullptr;
    myASCParams = nullptr;
    myNet = nullptr;
    myTLS = nullptr;
}

Network::~Network()
{
    delete myTLS;
}

bool
Network::PrivConnection(
    const ServerInfo& aServer
)
{
    OFStandard::initializeNetwork();

    std::string host_port = aServer.myHost + ":" + aServer.myPort;

    if (ASC_initializeNetwork(NET_REQUESTOR, 0, timeOut, &myNet) != EC_Normal) {
        std::cout << "ASC_initializeNetwork() failed" << std::endl;
        return false;
    }
    if (ASC_createAssociationParameters(&myASCParams, ASC_DEFAULTMAXPDU) != EC_Normal) {
        std::cout << "ASC_createAssociationParameters() failed" << std::endl;
        return false;
    }
    if (ASC_setAPTitles(myASCParams, MY_AETITLE, aServer.myTitle.c_str(), NULL) != EC_Normal) {
        std::cout << "ASC_setAPTitles() failed" << std::endl;
        return false;
    }
    if (ASC_setPresentationAddresses(myASCParams, OFStandard::getHostName().c_str(), host_port.c_str()) != EC_Normal) {
        std::cout << "ASC_setPresentationAddresses() failed" << std::endl;
        return false;
    }

    if (!myPrivateKey.empty()) {
        if (!PrivSupportTLS()) {
            std::cout << "PrivSupportTLS() failed" << std::endl;
            return false;
        }
    }

    return true;
}

bool
Network::PrivSupportTLS()
{
    myTLS = new DcmTLSTransportLayer(NET_REQUESTOR, mySeedFile.c_str(), true);
    if (nullptr == myTLS) {
        std::cout << "myTLS is nullptr" << std::endl;
        return false;
    }
    myTLS->initializeOpenSSL();

    if (!myPassPhrase.empty()) {
        myTLS->setPrivateKeyPasswd(myPassPhrase.c_str());
    }
    if (myTLS->setPrivateKeyFile(myPrivateKey.c_str(), DCF_Filetype_PEM) != EC_Normal) {
        std::cout << "setPrivateKeyFile() failed" << std::endl;
        return false;
    }
    if (myTLS->setCertificateFile(myCertifiFile.c_str(), DCF_Filetype_PEM) != EC_Normal) {
        std::cout << "setCertificateFile() failed" << std::endl;
        return false;
    }
    if (!myTLS->checkPrivateKeyMatchesCertificate()) {
        std::cout << "checkPrivateKeyMatchesCertificate() failed" << std::endl;
        return false;
    }
    if (CertificateVerification::DCV_checkCertificate == myCertify) {
        myTLS->setCertificateVerification(DCV_checkCertificate);
    } else if (CertificateVerification::DCV_ignoreCertificate == myCertify) {
        myTLS->setCertificateVerification(DCV_ignoreCertificate);
    } else if (CertificateVerification::DCV_requireCertificate == myCertify) {
        myTLS->setCertificateVerification(DCV_requireCertificate);
    }
    if (ASC_setTransportLayer(myNet, myTLS, false) != EC_Normal) {
        std::cout << "ASC_setTransportLayer() failed" << std::endl;
        return false;
    }
    if (ASC_setTransportLayerType(myASCParams, true) != EC_Normal) {
        std::cout << "ASC_setTransportLayerType() failed" << std::endl;
        return false;
    }

    return true;
}

bool
Network::SetTLS(
    const std::string& aSeedFile,
    const std::string& aPrivateKey,
    const std::string& aCertifiFile,
    const std::string& aPassPhrase,
    const CertificateVerification     aCertify
)
{
    mySeedFile = aSeedFile;
    myPrivateKey = aPrivateKey;
    myCertifiFile = aCertifiFile;
    myPassPhrase = aPassPhrase;
    myCertify = aCertify;

    return true;
}

bool
Network::PrivRelease(
    const ServerInfo& aServer
)
{
    if (ASC_releaseAssociation(myAssoc) != EC_Normal) {
        std::cout << "ASC_releaseAssociation() failed" << std::endl;
        return false;
    }
    if (ASC_destroyAssociation(&myAssoc) != EC_Normal) {
        std::cout << "ASC_destroyAssociation() failed" << std::endl;
        return false;
    }
    if (ASC_dropNetwork(&myNet) != EC_Normal) {
        std::cout << "ASC_dropNetwork() failed" << std::endl;
        return false;
    }

    OFStandard::shutdownNetwork();

    return true;
}

bool
Network::EchoRequest(
    const ServerInfo& aServer
)
{
    if (!PrivConnection(aServer)) {
        std::cout << "PrivConnection() failed" << std::endl;
        return false;
    }

    OFCondition cond;
    cond = ASC_addPresentationContext(
        myASCParams,
        1,
        UID_VerificationSOPClass,
        transferSyntax,
        transferSyntaxCount
    );
    if (cond != EC_Normal) {
        std::cout << "ASC_addPresentationContext() failed" << std::endl;
        return false;
    }
    if (0 == ASC_countPresentationContexts(myASCParams)) {
        std::cout << "ASC_countPresentationContexts() failed" << std::endl;
        return false;
    }
    if (ASC_requestAssociation(myNet, myASCParams, &myAssoc) != EC_Normal) {
        std::cout << "ASC_requestAssociation() failed" << std::endl;
        return false;
    }

    //c-echo
    T_DIMSE_C_EchoRQ req;
    req.MessageID = myAssoc->nextMsgID++;
    DIC_US status;
    cond = DIMSE_echoUser(
        myAssoc,
        req.MessageID,
        T_DIMSE_BlockingMode::DIMSE_BLOCKING,
        0,
        &status,
        nullptr
    );
    if (cond != EC_Normal) {
        std::cout << "DIMSE_echoUser() failed" << std::endl;
        return false;
    }

    if (!PrivRelease(aServer)) {
        std::cout << "PrivRelease() failed" << std::endl;
        return false;
    }

    return true;
}

bool
Network::StoreRequest(
    const ServerInfo& aServer,
    const std::string& aDCMFile
)
{
    std::ifstream iodStream(aDCMFile);
    if (!iodStream.is_open()) {
        std::cout << "wrong dicom file path" << std::endl;
        return false;
    }
    iodStream.seekg(0, std::ios::end);
    long fileSize = iodStream.tellg();

    if (!PrivConnection(aServer)) {
        std::cout << "PrivConnection() failed" << std::endl;
        return false;
    }

    OFCondition cond;
    cond = ASC_addPresentationContext(
        myASCParams,
        1,
        UID_MRImageStorage,
        transferSyntax,
        transferSyntaxCount
    );
    if (cond != EC_Normal) {
        std::cout << "ASC_addPresentationContext() failed" << std::endl;
        return false;
    }
    if (ASC_requestAssociation(myNet, myASCParams, &myAssoc) != EC_Normal) {
        std::cout << "ASC_requestAssociation() failed" << std::endl;
        return false;
    }

    //c-store
    T_ASC_PresentationContextID presId = ASC_findAcceptedPresentationContextID(myAssoc, UID_MRImageStorage);
    if (0 == presId) {
        std::cout << "ASC_findAcceptedPresentationContextID() failed" << std::endl;
        return false;
    }
    //request
    T_DIMSE_C_StoreRQ req;
    ZeroMemory(&req, sizeof(req));
    req.MessageID = myAssoc->nextMsgID++;
    DIC_UI uid = UID_MRImageStorage;
    memcpy(req.AffectedSOPClassUID, uid, sizeof(uid));
    req.Priority = T_DIMSE_Priority::DIMSE_PRIORITY_MEDIUM;
    req.DataSetType = T_DIMSE_DataSetType::DIMSE_DATASET_PRESENT;
    //response
    std::string instanceUid = Util::GenerateUid();
    OFStandard::strlcpy(req.AffectedSOPInstanceUID, instanceUid.c_str(), sizeof(instanceUid));
    T_DIMSE_C_StoreRSP rsp;
    DIMSE_StoreUserCallback callback = nullptr;
    if (Network::myStoreCallback) {
        callback = Network::PrivStoreCallback;
    }

    cond = DIMSE_storeUser(
        myAssoc,
        presId,
        &req,
        aDCMFile.c_str(),
        nullptr,
        callback,
        nullptr,
        T_DIMSE_BlockingMode::DIMSE_BLOCKING,
        0,
        &rsp,
        nullptr,
        nullptr,
        fileSize
    );
    if (cond != EC_Normal) {
        std::cout << "DIMSE_storeUser() failed" << std::endl;
        return false;
    }

    if (!PrivRelease(aServer)) {
        std::cout << "PrivRelease() failed" << std::endl;
        return false;
    }

    return true;
}

bool
Network::StoreRequest(
    const ServerInfo& aServer,
    std::shared_ptr<IDataSet>   aDataSet
)
{
    if (!PrivConnection(aServer)) {
        std::cout << "PrivConnection() failed" << std::endl;
        return false;
    }

    OFCondition cond;
    cond = ASC_addPresentationContext(
        myASCParams,
        1,
        UID_MRImageStorage,
        transferSyntax,
        transferSyntaxCount
    );
    if (cond != EC_Normal) {
        std::cout << "ASC_addPresentationContext() failed" << std::endl;
        return false;
    }
    if (ASC_requestAssociation(myNet, myASCParams, &myAssoc) != EC_Normal) {
        std::cout << "ASC_requestAssociation() failed" << std::endl;
        return false;
    }

    //c-store
    T_ASC_PresentationContextID presId = ASC_findAcceptedPresentationContextID(myAssoc, UID_MRImageStorage);
    if (0 == presId) {
        std::cout << "ASC_findAcceptedPresentationContextID() failed" << std::endl;
        return false;
    }
    //request
    T_DIMSE_C_StoreRQ req;
    ZeroMemory(&req, sizeof(req));
    req.MessageID = myAssoc->nextMsgID++;
    DIC_UI uid = UID_MRImageStorage;
    memcpy(req.AffectedSOPClassUID, uid, sizeof(uid));
    req.Priority = T_DIMSE_Priority::DIMSE_PRIORITY_MEDIUM;
    req.DataSetType = T_DIMSE_DataSetType::DIMSE_DATASET_PRESENT;
    const char* instanceUid;
    DcmDataset* dataset = aDataSet->GetDataSet();
    if (nullptr == dataset) {
        std::cout << "dataset is nullptr" << std::endl;
        return false;
    }
    if (dataset->findAndGetString(DCM_SOPInstanceUID, instanceUid) != EC_Normal) {
        std::cout << "find SOPInstanceUID failed" << std::endl;
        return false;
    }
    long dataSetLength = dataset->getLength();
    OFStandard::strlcpy(req.AffectedSOPInstanceUID, instanceUid, sizeof(instanceUid));
    //response
    T_DIMSE_C_StoreRSP rsp;
    DIMSE_StoreUserCallback callback = nullptr;
    if (Network::myStoreCallback) {
        callback = Network::PrivStoreCallback;
    }

    cond = DIMSE_storeUser(
        myAssoc,
        presId,
        &req,
        nullptr,
        dataset,
        callback,
        nullptr,
        T_DIMSE_BlockingMode::DIMSE_BLOCKING,
        0,
        &rsp,
        nullptr,
        nullptr,
        dataSetLength
    );
    if (cond != EC_Normal) {
        std::cout << "DIMSE_storeUser() failed" << std::endl;
        return false;
    }

    if (!PrivRelease(aServer)) {
        std::cout << "PrivRelease() failed" << std::endl;
        return false;
    }

    return true;
}

bool
Network::WorklistRequest(
    const ServerInfo& aServer,
    const WorklistInfo& aWorklist
)
{
    if (!PrivConnection(aServer)) {
        std::cout << "PrivConnection() failed" << std::endl;
        return false;
    }

    OFCondition cond;
    cond = ASC_addPresentationContext(
        myASCParams,
        1,
        UID_FINDModalityWorklistInformationModel,
        transferSyntax,
        transferSyntaxCount
    );
    if (cond != EC_Normal) {
        std::cout << "ASC_addPresentationContext() failed" << std::endl;
        return false;
    }
    if (ASC_requestAssociation(myNet, myASCParams, &myAssoc) != EC_Normal) {
        std::cout << "ASC_requestAssociation() failed" << std::endl;
        return false;
    }

    //c-find
    T_ASC_PresentationContextID presId = ASC_findAcceptedPresentationContextID(myAssoc, UID_FINDModalityWorklistInformationModel);
    if (0 == presId) {
        std::cout << "ASC_findAcceptedPresentationContextID() failed" << std::endl;
        return false;
    }

    DcmDataset dataset;
    if (dataset.putAndInsertOFStringArray(DCM_PatientName, aWorklist.myPatientName) != EC_Normal) {
        std::cout << "put PatientName failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientID, aWorklist.myPatientID) != EC_Normal) {
        std::cout << "put PatientID failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientSex, aWorklist.myPatientSex) != EC_Normal) {
        std::cout << "put PatientSex failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientBirthDate, aWorklist.myPatientBirthDate) != EC_Normal) {
        std::cout << "put PatientBirthDate failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientSize, aWorklist.myPatientSize) != EC_Normal) {
        std::cout << "put PatientSize failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientWeight, aWorklist.myPatientWeight) != EC_Normal) {
        std::cout << "put PatientWeight failed" << std::endl;
        return false;
    }
    if (dataset.putAndInsertOFStringArray(DCM_PatientAge, aWorklist.myPatientAge) != EC_Normal) {
        std::cout << "put PatientAge failed" << std::endl;
        return false;
    }
    DcmSequenceOfItems* sequence = new DcmSequenceOfItems(DCM_ScheduledProcedureStepSequence);
    DcmItem* item = new DcmItem();
    if (nullptr == sequence || nullptr == item) {
        delete sequence;
        delete item;
        std::cout << "sequence or item is nullptr" << std::endl;
        return false;
    }
    if (item->putAndInsertOFStringArray(DCM_ScheduledStationAETitle, aWorklist.myScheduledStationAETitle) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "put Modality failed" << std::endl;
        return false;
    }
    if (item->putAndInsertOFStringArray(DCM_ScheduledProcedureStepStartDate, aWorklist.myScheduledProcedureStepStartDate) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "put Modality failed" << std::endl;
        return false;
    }
    if (item->putAndInsertOFStringArray(DCM_ScheduledProcedureStepStartTime, aWorklist.myScheduledProcedureStepStartTime) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "put Modality failed" << std::endl;
        return false;
    }
    if (item->putAndInsertOFStringArray(DCM_Modality, aWorklist.myModality) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "put Modality failed" << std::endl;
        return false;
    }
    if (sequence->insert(item) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "ScheduledProcedureStepSequence insert item failed" << std::endl;
        return false;
    }
    if (dataset.insert(sequence) != EC_Normal) {
        delete sequence;
        delete item;
        std::cout << "ScheduledProcedureStepSequence insert sequence failed" << std::endl;
        return false;
    }

    int responseCount = 0;
    DcmDataset* statusDetail = nullptr;
    //request
    T_DIMSE_C_FindRQ request;
    request.MessageID = myAssoc->nextMsgID++;
    DIC_UI uid = UID_FINDModalityWorklistInformationModel;
    OFStandard::strlcpy(request.AffectedSOPClassUID, uid, sizeof(uid));
    request.Priority = DIMSE_PRIORITY_MEDIUM;
    request.DataSetType = DIMSE_DATASET_PRESENT;
    //response
    T_DIMSE_C_FindRSP response;
    DIMSE_FindUserCallback callback = nullptr;
    if (Network::myWorklistCallback) {
        callback = Network::PrivWorklistCallback;
    }

    cond = DIMSE_findUser(
        myAssoc,
        presId,
        &request,
        &dataset,
        responseCount,
        callback,
        nullptr,
        T_DIMSE_BlockingMode::DIMSE_BLOCKING,
        0,
        &response,
        &statusDetail
    );
    if (cond != EC_Normal) {
        std::cout << "DIMSE_findUser() failed" << std::endl;
        return false;
    }

    if (!PrivRelease(aServer)) {
        std::cout << "PrivRelease() failed" << std::endl;
        return false;
    }

    return true;
}

void
Network::PrivStoreCallback(
    void* aCallbackData,
    T_DIMSE_StoreProgress* aProgress,
    T_DIMSE_C_StoreRQ* aRequest
)
{
    NetworkStatus status = NetworkStatus::Start;
    double percent = 0.0;

    if (T_DIMSE_StoreProgressState::DIMSE_StoreBegin == aProgress->state) {
        status = NetworkStatus::Start;
        percent = 0;
    } else if (T_DIMSE_StoreProgressState::DIMSE_StoreProgressing == aProgress->state) {
        status = NetworkStatus::Pending;
        percent = std::floor(aProgress->progressBytes * 100.0 / aProgress->totalBytes);
    } else if (T_DIMSE_StoreProgressState::DIMSE_StoreEnd == aProgress->state) {
        status = NetworkStatus::End;
        percent = 100;
    }

    Network::myStoreCallback(status, percent, "");
}

void
Network::PrivWorklistCallback(
    void* aCallbackData,
    T_DIMSE_C_FindRQ* aRequest,
    int aResponseCount,
    T_DIMSE_C_FindRSP* aResponse,
    DcmDataset* aResponseIdentifiers
)
{
    NetworkStatus status = NetworkStatus::Start;
    std::string responseValue;
    std::string responseStr("{");

    if (STATUS_FIND_Pending_MatchesAreContinuing == aResponse->DimseStatus) {
        DcmObject* obj = aResponseIdentifiers->nextInContainer(nullptr);
        while (obj)
        {
            DcmTag tag = obj->getTag();
            std::string tagName = tag.getTagName();
            unsigned short group = tag.getGTag();
            unsigned short element = tag.getETag();
            if ((0x0008 == group && 0x0005 == element)//SpecificCharacterSet​
                || (0x0008 == group && 0x0201 == element)//TimezoneOffsetFromUTC​
                || (0x0040 == group && 0x0100 == element)//ScheduledProcedureStepSequence​​
            ) {
                obj = aResponseIdentifiers->nextInContainer(obj);
                continue;
            }
            if (aResponseIdentifiers->findAndGetOFString(DcmTagKey(group, element), responseValue) != EC_Normal) {
                status = NetworkStatus::Failure;
                break;
            }
            if (responseStr != "{") {
                responseStr += ",";
            }
            responseStr += "\"" + tagName + "\"" + ":" + "\"" + responseValue + "\"";

            obj = aResponseIdentifiers->nextInContainer(obj);
        }
        responseStr += "}";
        //support gbk && utf8
        try {
            json tmp = json::parse(responseStr);
        } catch (nlohmann::json::exception& e) {
            responseStr = Util::GBK2UTF8(responseStr.c_str());
        }
        if (status != NetworkStatus::Failure) {
            status = NetworkStatus::Pending;
        }
    } else {
        status = NetworkStatus::Failure;
    }

    Network::myWorklistCallback(status, responseStr, "");
}

void
Network::BindWorklistCallback(
    PWorklistCallback aCallBack
)
{
    Network::myWorklistCallback = aCallBack;
}

void
Network::BindStoreCallback(
    PStoreCallback aCallBack
)
{
    Network::myStoreCallback = aCallBack;
}

std::string
Network::GetOpenSSLVersion()
{
    return DcmTLSTransportLayer().getOpenSSLVersionName();
}
